﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using Common.Logging;
using SimpleServiceBus.Bus;
using SimpleServiceBus.Bus.Pipeline;
using SimpleServiceBus.Messages.Management.Monitoring;

namespace SimpleServiceBus.Endpoint.Management.Monitoring.Profiling
{
    public class EndpointProfilerService : EndpointServiceBase, IEndpointProfilerService
    {
        [ThreadStatic] private static PipelineProfiler _currentSendProfiler;
        [ThreadStatic] private static PipelineProfiler _currentReceiveProfiler;

        private readonly Dictionary<string, EndpointMeasurementSummary> _measurements =
            new Dictionary<string, EndpointMeasurementSummary>(StringComparer.OrdinalIgnoreCase);

        private readonly object _measurementLock = new object();

        public EndpointProfilerService()
        {
            Enabled = true;
            PipelineDirection = PipelineDirection.Both;
        }

        public bool Enabled { get; set; }
        public bool ProfilePipelineComponents { get; set; }
        public PipelineDirection PipelineDirection { get; set; }

        private readonly Stopwatch _profileDuration = new Stopwatch();
        
        
        public IList<EndpointMeasurement> GetProfilerMeasurements()
        {
            return GetProfilerMeasurements(true);
        }

        public IList<EndpointMeasurement> GetProfilerMeasurements(bool reset)
        {
            var measurements = new List<EndpointMeasurement>();

            lock(_measurementLock)
            {
                _profileDuration.Stop();
                foreach(string measurementName in _measurements.Keys)
                {
                    EndpointMeasurementSummary summary = _measurements[measurementName];
                    CreateMeasurementsFromSummary(summary, measurements);
                }

                if (reset)
                {
                    _measurements.Clear();
                    _profileDuration.Reset();
                    _profileDuration.Start();
                }
                else
                {
                    _profileDuration.Start();
                }
            }

            return measurements;
        }

        protected override void StartService()
        {
            _profileDuration.Start();

            Endpoint.MessageBus.BeginReceiveMessage += MessageBusBeginReceiveMessage;
            Endpoint.MessageBus.MessageReceived += MessageBusMessageReceived;

            Endpoint.MessageBus.BeginSendMessage += MessageBusBeginSendMessage;
            Endpoint.MessageBus.MessageSent += MessageBusMessageSent;


        }
       

        protected override void ShutDownService()
        {
            lock (_measurementLock)
            {
                if (Endpoint != null && Endpoint.MessageBus != null)
                {
                    Endpoint.MessageBus.BeginReceiveMessage -= MessageBusBeginReceiveMessage;
                    Endpoint.MessageBus.MessageReceived -= MessageBusMessageReceived;

                    Endpoint.MessageBus.BeginSendMessage -= MessageBusBeginSendMessage;
                    Endpoint.MessageBus.MessageSent -= MessageBusMessageSent;
                }
                _measurements.Clear();
            }
        }

        void MessageBusBeginSendMessage(object sender, MessageProcessingEventArgs e)
        {
            if (Enabled == false || PipelineDirection == PipelineDirection.Receive || e.Message == null)
                return;

            var args = e as MessagePipelineEventArgs;
            if (args != null)
            {
                _currentSendProfiler = new PipelineProfiler(args.Pipeline,ProfilePipelineComponents);
            }
        }

        void MessageBusMessageSent(object sender, MessageProcessingEventArgs e)
        {
            if (_currentSendProfiler != null)
                RecordProfilerResults(_currentSendProfiler.Result, PipelineDirection.Send);
            _currentSendProfiler = null;
        }

        void MessageBusBeginReceiveMessage(object sender, MessageProcessingEventArgs e)
        {
            if (Enabled == false || PipelineDirection == PipelineDirection.Send || e.Message == null)
                return;

            var args = e as MessagePipelineEventArgs;
            if (args != null)
            {
                _currentReceiveProfiler = new PipelineProfiler(args.Pipeline,ProfilePipelineComponents);
            }
        }

        void MessageBusMessageReceived(object sender, MessageProcessingEventArgs e)
        {
            if (_currentReceiveProfiler != null)
                RecordProfilerResults(_currentReceiveProfiler.Result, PipelineDirection.Receive);
            _currentReceiveProfiler = null;
        }

        private static string PrefixFor(PipelineDirection direction)
        {
            return direction.ToString();
        }

        private void RecordProfilerResults(PipelineProfilerResult profilerResult, PipelineDirection direction)
        {
            Action<PipelineProfilerResult, PipelineDirection> del = SafeRecordProfilerResults;
            del.BeginInvoke(profilerResult, direction, null, null);
            
        }

        private void SafeRecordProfilerResults(PipelineProfilerResult profilerResult,PipelineDirection direction)
        {
            try
            {
                string global = PrefixFor(direction);

                RecordProfilerResults(global + ".All", profilerResult);

                if (profilerResult.MessageType != null)
                    RecordProfilerResults(global + "." + profilerResult.MessageType.Name, profilerResult);

            }
            catch (Exception ex)
            {

                ILog logger = LogManager.GetLogger(typeof (EndpointProfilerService));
                logger.Error("An error occurred recording the results of the endpoint profiler:" + ex.Message, ex);
            }
        }

        private void RecordProfilerResults(string prefix, PipelineProfilerResult profilerResult)
        {
            //This odd looking call will allow us to keep a running total
            //of messages processed that will easily convert
            //to measurements the probing mechanism can make use of.
            RecordMeasurement(prefix + ".MessageCount", "messages", 1);
            
            RecordMeasurement(prefix + ".ProcessingTime", "ms", profilerResult.ProcessingTime.TotalMilliseconds);
            RecordMeasurement(prefix + ".MessageSize", "bytes", profilerResult.MessageSize);
                
            foreach (PipelineComponentProfilerResult component in profilerResult.Components)
            {
                RecordMeasurement(prefix + ".ProcessingTime." + component.ComponentName,
                                  "ms",component.ProcessingTime.TotalMilliseconds);

            }
        }

        private  void RecordMeasurement(string measurementKey, string unit, double value)
        {
            EndpointMeasurementSummary summary;

            //We'll lock the dictionary to fetch an individual summary
            lock(_measurementLock)
            {
                if(_measurements.ContainsKey(measurementKey))
                      summary = _measurements[measurementKey];
                else
                {
                    
                    summary = new EndpointMeasurementSummary();
                    _measurements[measurementKey] = summary;
                }
            }

            //Now we need to lock the summary itself. We're using a different lock than
            //the dictionary lock above so we don't block processing of other kinds 
            //of profile results.
            lock(summary)
            {
                var measurement = new EndpointMeasurement
                                      {
                                          CanAggregate = true,
                                          DataType = MeasurementDataType.Numeric,
                                          Name = measurementKey,
                                          Timestamp = DateTime.Now.ToUniversalTime(),
                                          UnitOfMeasurement = unit,
                                          Value = value.ToString()
                                      };
                summary.AddMeasurement(measurement);
            }
        }



        private void CreateMeasurementsFromSummary(EndpointMeasurementSummary summary,ICollection<EndpointMeasurement> measurements)
        {
            if (summary.LastMeasurement == null)
                return;
            
            if (summary.LastMeasurement.Name.Contains("MessageCount"))
            {
                measurements.Add(CreateMeasurementFor(summary, null, s => s.SampleCount));

                EndpointMeasurement rate = CreateMeasurementFor(summary, "MessageRate",
                                                                s => s.SampleCount/_profileDuration.Elapsed.TotalMinutes);
                rate.UnitOfMeasurement = "messages/minute";
                measurements.Add(rate);
            }
            else
            {
                measurements.Add(CreateMeasurementFor(summary, "Average", s => s.Average));
                measurements.Add(CreateMeasurementFor(summary, "Max", s => s.Max));
            }
        }

        private static EndpointMeasurement CreateMeasurementFor(EndpointMeasurementSummary summary,string name,Func<EndpointMeasurementSummary,double> value)
        {
            if (name != null) name = "." + name;

            return new EndpointMeasurement
                       {
                           CanAggregate = summary.LastMeasurement.CanAggregate,
                           DataType = summary.LastMeasurement.DataType,
                           Name = summary.LastMeasurement.Name + name ?? String.Empty,
                           Timestamp = DateTime.Now.ToUniversalTime(),
                           UnitOfMeasurement = summary.LastMeasurement.UnitOfMeasurement,
                           Value = value != null ? value(summary).ToString() : null
                       };
        }
    }
}
